package com.ly.recommend;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author : 黑渊白花
 * @ClassName UserCF
 * @date : 2025/5/10 20:18
 * @Description
 */

public class UserCF {

    private static double calculateSimilarity(Map<Integer, Double> userRatings, Map<Integer, Double> otherUserRatings) {
        // 初始化用于存储用户评分向量点积的变量，初始值为0.0，后续会根据共同评分的物品来累加计算
        double dotProduct = 0.0;
        // 初始化用于存储当前用户评分向量模长平方的变量，初始值为0.0，后续会根据当前用户的评分来累加计算
        double userMagnitude = 0.0;
        // 初始化用于存储其他用户评分向量模长平方的变量，初始值为0.0，后续会根据其他用户的评分来累加计算
        double otherUserMagnitude = 0.0;
        // 遍历当前用户的评分数据，计算与其他用户评分数据的点积以及各自向量的模长平方
        for (Map.Entry<Integer, Double> entry : userRatings.entrySet()) {
            // 获取当前评分数据对应的物品ID
            int itemId = entry.getKey();
            // 如果其他用户也对该物品进行了评分（即otherUserRatings中包含该物品ID），则进行以下计算
            if (otherUserRatings.containsKey(itemId)) {
                // 计算点积，将当前用户对该物品的评分与其他用户对该物品的评分相乘，并累加到dotProduct变量中
                dotProduct += entry.getValue() * otherUserRatings.get(itemId);
                // 计算当前用户评分向量模长的平方，将当前用户对该物品的评分进行平方，并累加到userMagnitude变量中
                userMagnitude += Math.pow(entry.getValue(), 2);
                // 计算其他用户评分向量模长的平方，将其他用户对该物品的评分进行平方，并累加到otherUserMagnitude变量中
                otherUserMagnitude += Math.pow(otherUserRatings.get(itemId), 2);
            }
        }
        // 如果当前用户评分向量模长的平方为0或者其他用户评分向量模长的平方为0，
        // 说明至少有一个用户没有对任何共同物品进行评分，此时相似度为0，直接返回0（防止后续除法运算出现除以零的错误）
        if (userMagnitude == 0 || otherUserMagnitude == 0) {
            return 0;
        }
        // 根据余弦相似度的计算公式，返回两个用户之间的相似度得分。
        // 即点积除以两个用户评分向量模长的乘积（先分别对模长平方开方得到模长，再相乘）
        return dotProduct / (Math.sqrt(userMagnitude) * Math.sqrt(otherUserMagnitude));
    }

    public static List<Integer> recommend(Map<Integer, Map<Integer, Double>> ratings, int userId, int numRecommendations) {
        // 创建一个Map，用于存储每个物品的加权评分。键为物品ID，值为加权评分（初始化为0.0）。
        // 加权评分会综合考虑其他用户与目标用户的相似度以及他们对物品的评分来计算。
        Map<Integer, Double> weightedRatings = new HashMap<>();
        // 创建一个Map，用于存储每个其他用户与目标用户的相似度得分。键为其他用户的ID，值为相似度得分（初始化为0.0）。
        Map<Integer, Double> similarityScores = new HashMap<>();

        // 计算与其他用户的相似度，并基于相似度和其他用户的评分计算物品的加权评分
        for (Map.Entry<Integer, Map<Integer, Double>> entry : ratings.entrySet()) {
            // 获取当前遍历到的其他用户的ID
            int otherUserId = entry.getKey();
            // 如果当前用户就是目标用户，则跳过本次循环，不需要计算与自身的相似度
            if (otherUserId == userId) continue;

            // 调用calculateSimilarity方法计算当前其他用户与目标用户的相似度，并将结果存入similarityScores Map中
            double similarity = calculateSimilarity(ratings.get(userId), entry.getValue());
            similarityScores.put(otherUserId, similarity);

            // 遍历当前其他用户评分过的物品，计算这些物品的加权评分
            for (Map.Entry<Integer, Double> item : entry.getValue().entrySet()) {
                // 获取物品的ID
                int itemId = item.getKey();
                // 获取当前其他用户对该物品的评分
                double rating = item.getValue();

                // 如果目标用户没有对该物品进行过评分（即ratings.get(userId).get(itemId)为null），
                // 则将该物品的加权评分加上当前其他用户对该物品的评分乘以相似度得分
                if (ratings.get(userId).get(itemId) == null) {
                    weightedRatings.put(itemId, weightedRatings.getOrDefault(itemId, 0.0) + rating * similarity);
                }
            }
        }

        // 将加权评分的Map转换为List<Map.Entry<Integer, Double>>，方便后续进行排序操作
        // 每个元素代表一个物品的ID及其对应的加权评分，以键值对形式存储在Entry中
        List<Map.Entry<Integer, Double>> sortedRecommendations = new ArrayList<>(weightedRatings.entrySet());
        // 使用lambda表达式自定义排序规则，按照加权评分从高到低对推荐物品进行排序
        // 这里通过比较Entry中的值（即加权评分）来确定顺序，b.getValue().compareTo(a.getValue())表示按照降序排列
        sortedRecommendations.sort((a, b) -> b.getValue().compareTo(a.getValue()));

        // 创建一个列表，用于存储最终要返回的推荐物品ID
        List<Integer> recommendedItems = new ArrayList<>();
        // 根据期望推荐的数量（取numRecommendations和实际可推荐物品数量的较小值），
        // 将排序后的推荐物品ID依次添加到recommendedItems列表中
        for (int i = 0; i < Math.min(numRecommendations, sortedRecommendations.size()); i++) {
            recommendedItems.add(sortedRecommendations.get(i).getKey());
        }
        return recommendedItems;
    }


}
